//EvaluationService翻译为中文是评估服务 所以初步判断这个js是处理评估逻辑的

import axios from 'axios'; //这是一个用于发送网络请求的插件（个人理解）
import { jsPDF } from 'jspdf'; //用于在网页中动态生成 PDF 文件
import 'jspdf-autotable'; //用于自动创建表格

// 先折叠起来 一个一个看

//mapScoresToIndicatorNames: 将分数映射到指标（键值对）。传入两个参数 bScores和indicators分别代表分数和指标
function mapScoresToIndicatorNames(bScores, indicators) {

    //part1. 先对传入的数据进行有效性检查

    //控制台输出分数和指标(估计是用来调试用的 忘记删了)
    console.log("bScores:", bScores);
    console.log("indicators:", indicators);
    //如果传入的分数是假值（null、undefined、0、false、空字符串等）
    //或者传入的的类型不是对象类型（或许有个疑惑这里为什么是!==而不是!=  记住一般用三个等号就行，具体原因自行搜索）
    if (!bScores || typeof bScores !== 'object') {
        console.error("无效的 bScores:", bScores); //控制台输出无效 返回空
        return {};
    }
    //如果传入的指标是假值 Array.isArray是js中的内置方法 用来判断一个变量是否是数组。这里是 如果传入的指标不是一个数组
    if (!indicators || !Array.isArray(indicators)) {
        console.error("无效的 indicators:", indicators);
        return {};
    }


    // part2. 将分数映射到指标（函数实现部分）
    const mappedScores = {}; // 定义了一个空对象，啥是空对象 就是啥都没放的对象（听君一席话。。。
    indicators.forEach(indicator => { //forEach用来遍历数组的 js内置函数，开箱即用，
        const match = indicator.name.match(/\((B\d+)\)/); //分两部分看，选中indicator.name，然后调用match函数选中，用啥来选选中？正则表达式，正则表达式在哪里？括号内这一坨： /\((B\d+)\)/  干了啥 匹配呗 提取其中的 "B+数字"
        if (match && bScores[match[1]] !== undefined) {  //如果选中成功并且Scores[match[1]]不是未定义类型
            mappedScores[indicator.name] = bScores[match[1]]; //这里是一个键值对 存储东西 indicator.name是键 bScores[match[1]]是值，作为映射结果
            console.log(`映射成功: ${indicator.name} -> ${bScores[match[1]]}`);
        } else {
            console.warn(`无法映射指标: ${indicator.name}, match: ${match}, bScores[${match?.[1]}]: ${bScores[match?.[1]]}`);
        }
    });

    console.log("映射后的分数:", mappedScores);
    return mappedScores;
}



// evaluateDevice:评估设备。 参数是deviceInput：设备输入；indicators：指标，primaryWeights：权重
export async function evaluateDevice(deviceInput, indicators, primaryWeights) {
    //part1.判断输入数据的合法性
    if (!deviceInput || !deviceInput.name || !deviceInput.model || !deviceInput.initialValue || !deviceInput.bScores) {
        console.error("无效的 deviceInput:", deviceInput);
        throw new Error("设备输入数据无效");
    }

    if (!indicators || !Array.isArray(indicators) || indicators.length === 0) {
        console.error("无效的 indicators:", indicators);
        throw new Error("评估指标未加载");
    }

    if (!primaryWeights || typeof primaryWeights !== 'object') {
        console.error("无效的 primaryWeights:", primaryWeights);
        throw new Error("评估权重未加载");
    }

    console.log("评估设备输入:", deviceInput);
    console.log("评估指标:", indicators);
    console.log("评估权重:", primaryWeights);


    //part2. 设备分数映射
    const scoresForApi = mapScoresToIndicatorNames(deviceInput.bScores, indicators); //调用前面的分数映射函数
    console.log("映射后的分数:", scoresForApi);

    if (!scoresForApi || Object.keys(scoresForApi).length === 0) { //再判断一次分数是否合法。Object.keys用于返回传入对象的所有键，如果键的长度是0的话，那就说明这个对象是空的
        console.error("无法映射分数，scoresForApi 为空:", scoresForApi);
        return {
            ...deviceInput, //哇靠 三个点是麻玩意儿，这是js语法中的拓展运算符 用来展开对象或者数组，这里是展开对象的所有属性（也就是展开键值对）；官方一点说就是浅拷贝，拷贝对象包含原对象的所有数据 同时允许添加或者修改新属性
            evaluation: null,
            error: "评估失败: 无法映射设备分数，请检查 bScores 和 indicators",
        };
    }


    // part3 构造api请求
    const requestData = {  //创建一个对象 
        scores: scoresForApi,  //属性一 分数
        originalPrice: parseFloat(deviceInput.initialValue),  //属性二  原价
        secondaryIndicators: indicators, //属性三 次级指标
    };

    console.log("发送的 API 请求数据:", requestData);

    try {
        const response = await axios.post('/api/evaluation/calculate', requestData); //axios.post用于向服务器发送请求 参数是url（'/api/evaluation/calculate'）和json（requestData）这里总的就是 将requestData发送到/api/evaluation/calculate
        console.log(`评估设备 ${deviceInput.name} (${deviceInput.model}) 成功:`, response.data); //成功的时候 服务器包含的数据会存放在response.data中 $是变量引用，配合{}是解构，比如这里的${deviceInput.name} 就是解构response.data的name属性存放到deviceInput.name
        return {
            ...deviceInput,
            evaluation: response.data,
        };
    } catch (error) {
        const errorMessage = error.response?.data?.message || error.message || '未知错误，请联系管理员';
        console.error(`评估设备 ${deviceInput.name} (${deviceInput.model}) 失败:`, errorMessage);
        return {
            ...deviceInput,
            evaluation: null,
            error: `评估失败: ${errorMessage}`,
        };
    }
}

//async是什么东西？在函数前面加上这个关键字之后 函数里面的return就会被包装成Promise
// 传统的异步处理中（不知道异步就去搜），Promise处理方式中通常使用then链式调用，使用async可以让代码更清晰
//还是不理解就跳过 学的多了后面再回头看就会了
async function loadFont(pdfInstance) {  //看函数名理解函数，loadFont顾名思义加载字体
    try {
        const fontUrl = '/fonts/simhei.ttf';
        const response = await fetch(fontUrl); //await是啥，是async函数中用来暂停的，直到异步任务结束才继续执行后面的代码 那你问我，这里的异步任务是啥？上面的'/fonts/simhei.ttf'就是一个异步任务：请求字体文件，直到这个文件请求到，然后被fetch函数捕获结果 才继续执行后面的代码
        if (!response.ok)  //如果请求失败（ok是response的一个属性 是布尔类型）
            throw new Error(`字体加载失败: ${response.statusText}`); // throw用于终止执行并抛出错误 错误放在哪里？直接new Error一个对象，里面放错误信息（response.statusText 访问response的statusText属性，表示状态文本 比如Not Found）
        const fontBlob = await response.blob(); //调用blob函数 将上面的statustext转换为二进制数据


        const reader = new FileReader(); // 创建一个FileReader类型的大象（对象哈），
        const base64String = await new Promise((resolve, reject) => { //拿到一个函数调用 先看内部干了啥再看外部
            //其实就是相当于三个判断逻辑 文件读取成功 文件读取错误 文件读取完成

            reader.onloadend = () => resolve(reader.result.split(',')[1]); //好复杂啊 看的头晕...其实这里 是这么理解的：reader是FileReader创建的对象实例 所以它拥有FileReader属性，方法和事件  这和java中的类创建实例是相同的
            //于是这里的onloadend是FileReader的事件，result是属性 ，于是整个函数的作用是（从后面往前面看）：reader.result(存储base64)，调用split分割字符串，怎么分割？逗号分割，分割完成后选择第二部分（[1]是索引，索引从0开始， 所以[1]是第二部分）。在文件读取完成时（onloadend）赋值

            reader.onerror = reject;   //onerror事件，在文件读取错误的时候触发，reject是promise的回调函数
            reader.readAsDataURL(fontBlob); //readAsDataURL文件读取完成触发
        });
        pdfInstance.addFileToVFS('simhei.ttf', base64String);
        pdfInstance.addFont('simhei.ttf', 'simhei', 'normal');
        pdfInstance.setFont('simhei');
        return true;
    }
    //上面的try不是执行了一些任务么 但是执行任务的时候可能出错 于是使用catch对try中的错误进行处理 错误不处理会影响整个程序的哦
    catch (error) {
        console.error("加载 PDF 字体失败:", error);
        alert("无法加载 PDF 所需字体，报告可能无法正确生成。");
        return false;
    }
}

//下面两个函数 generateSingleReport和generateBatchReports 是以导出开头的 分别是导出单个文件报告和多个文件报告

//generateSingleReport生成单个报告。参数evaluatedDeviceData（评估设备数据），indicators（指标），primaryWeights（权重）
export async function generateSingleReport(evaluatedDeviceData, indicators, primaryWeights) {
    const { name, model, initialValue, evaluation, error } = evaluatedDeviceData; // 这里使用了解构语句，即{},从evaluatedDeviceData对象中提取属性分别赋值到name, model, initialValue, evaluation, error

    if (error || !evaluation) {
        alert(`设备 ${name} (${model}) 存在评估错误，无法生成报告。`);
        return;
    }

    const pdf = new jsPDF('p', 'mm', 'a4'); //创建一个jsPDF对象，这种带参数的创建对象语，参数是传递给了构造函数。麻是构造函数，就是在创建对象的时候会自动调用的一个函数，不需要你去调用。查询了一下，这个jsPDF构造函数的三个参数是：orientation（方向 p纵向 l横向），unit（单位），format（纸张大小），所以这里是 纵向(p)  mm（毫米） a4(a4纸)
    if (!(await loadFont(pdf))) // 如果无法加载字体????这和加载字体有啥关系 其实打开loadFont函数查看传入的参数的实际使用就发现，是加载字体到PDF文档用于显示中文....所以那个函数名取名有点小瑕疵，我觉得叫loadFontToPdf更加合适（加载字体到pdf），初看那个函数我以为是加载字体到浏览器显示
        return;

    const reportTitle = '电力装备逆向物流价值评估报告';
    const generationTime = `生成时间: ${new Date().toLocaleString()}`;
    const reportCode = `REPORT-${model}-${Date.now().toString().slice(-6)}`;
    const reportCodeText = `报告编码: ${reportCode}`;
    const deviceModelText = `设备型号: ${model || '-'}`;
    const deviceNameText = `设备名称: ${name || '-'}`;
    const originalPriceText = `设备原价值: ${initialValue ? parseFloat(initialValue).toFixed(2) : '-'} 元`;
    let startY = 15;


    //下面的代码用于生成一个PDF文件
    pdf.setFontSize(18); //设置字体大小
    pdf.text(reportTitle, pdf.internal.pageSize.getWidth() / 2, startY, { align: 'center' }); //设置标题在页面上居中显示，text用于插入文本 有哪些参数？text(reportTitle)-要插入的文本 x(pdf.internal.pageSize.getWidth() / 2)-横坐标 y(startY)-纵坐标 opsiton({ align: 'center' })-居中
    startY += 10; // 向下移动十个单位 单位是mm 为什么是毫米？因为 const pdf = new jsPDF 创建对象的时候传入了

    pdf.setFontSize(10);
    pdf.text(generationTime, 14, startY);
    pdf.text(reportCodeText, pdf.internal.pageSize.getWidth() - 14, startY, { align: 'right' });
    startY += 5;
    pdf.text(deviceNameText, 14, startY);
    startY += 5;
    pdf.text(deviceModelText, 14, startY);
    startY += 5;
    pdf.text(originalPriceText, 14, startY);
    startY += 8;

    pdf.line(14, startY, pdf.internal.pageSize.getWidth() - 14, startY);
    startY += 8;

    pdf.setFontSize(14);
    pdf.text('指标评估明细', 14, startY);
    startY += 6;

    const indicatorHead = [/* ... same headers ... */];
    //整个函数的作用是将indicators 转换为结构化的 indicatorBody 数组
    //具体实现过程看下面
    //还是那句话 看不懂就不要深入 浏览一下大致结构就行
    const indicatorBody = indicators.map( //map函数用于遍历一个数组并且返回一个新的数组


        //下面这个ind其实相当于
        // for(int i=0;i<indicators.length;i++)
        // const ind = indicators[i]
        ind => {
            //measuredValue-测量值   evaluatedDeviceData.bScores-设备评估分数
            const measuredValue = evaluatedDeviceData.bScores[ind.name.match(/\((B\d+)\)/)?.[1]] ?? '-';
            return [
                ind.primaryIndicator, //主要指标
                ind.name, //指标名
                measuredValue,
                ind.unit || '分', //单位
                evaluation.normalizedValues[ind.name]?.toFixed(4) || '-', // 归一化后的指标值，保留 4 位小数   归一化就是调整数据范围
                primaryWeights[ind.primaryIndicator]?.toFixed(4), // 主要指标的权重，保留 4 位小数
                ind.weight?.toFixed(4), // 当前指标的权重，保留 4 位小数
                ind.weightedWeight?.toFixed(4) || '-', // 计算后的加权权重，保留 4 位小数，默认 `'-'` 表示无数据
                evaluation.weightedScores[ind.name]?.toFixed(4) || '-', // 该指标最终计算出的加权得分，保留 4 位小数，默认 `'-'` 表示无数据
            ];
        });

    pdf.autoTable({
        startY: startY, // 表格的起始 Y 轴位置，通常用于控制表格的垂直位置
        head: indicatorHead, // 表头数据，定义表格的标题行
        body: indicatorBody, // 表格的主体数据，包含各项指标信息
        headStyles: {
            fillColor: [220, 220, 220], // 表头的背景颜色（浅灰色）
            textColor: 0, // 表头文字颜色（黑色）
            fontStyle: 'bold', // 表头文字加粗
            font: 'simhei' // 使用 "SimHei" 字体
        },
        styles: {
            fontSize: 7, // 设置表格字体大小为 7
            cellPadding: 1, // 单元格内边距，确保文字不贴边
            font: 'simhei', // 设定 "SimHei" 字体
            fontStyle: 'normal' // 正文字体风格为普通（非加粗、斜体）
        },
        columnStyles: {
            /* 这里可以定义每列的样式，例如对齐方式、宽度等 */
        },
        didDrawPage: (data) => {
            pdf.setFont('simhei'); // 在绘制完当前页面后，确保字体仍为 "SimHei"
        }
    });


    startY = pdf.lastAutoTable.finalY + 10;

    pdf.line(14, startY, pdf.internal.pageSize.getWidth() - 14, startY);
    startY += 8;
    pdf.setFontSize(14);
    pdf.text('总得分与评估等级', 14, startY);
    startY += 6;
    pdf.setFontSize(10);
    pdf.text(`总得分: ${evaluation.totalScore ? evaluation.totalScore.toFixed(4) : '-'}`, 14, startY);
    startY += 5;
    pdf.text(`装备状态: ${evaluation.grade || '-'}级`, 14, startY);
    startY += 8;

    pdf.line(14, startY, pdf.internal.pageSize.getWidth() - 14, startY);
    startY += 8;
    pdf.setFontSize(14);
    pdf.text('装备剩余价值', 14, startY);
    startY += 6;
    pdf.setFontSize(12);
    pdf.text(` ${evaluation.finalRealPrice !== null ? evaluation.finalRealPrice.toFixed(2) : '-'} 元`, 14, startY);
    startY += 10;

    const criteriaHead = [/* ... */];
    const criteriaBody = [/* ... */];
    pdf.autoTable({ startY: startY, head: criteriaHead, body: criteriaBody, /* ... styles ... */ });
    startY = pdf.lastAutoTable.finalY + 10;

    pdf.save(`评估报告-${name}_${model}-${reportCode}.pdf`);
}


export async function generateBatchReports(evaluatedDevices, indicators, primaryWeights) {
    if (!evaluatedDevices || evaluatedDevices.length === 0) {
        alert("没有可生成报告的设备。");
        return;
    }

    const devicesToReport = evaluatedDevices.filter(d => d.evaluation && !d.error);
    if (devicesToReport.length === 0) {
        alert("所有设备均存在评估错误，无法生成报告。");
        return;
    }

    alert(`即将为 ${devicesToReport.length} 台设备生成报告，请允许浏览器下载多个文件。`);

    for (let i = 0; i < devicesToReport.length; i++) {
        try {
            await generateSingleReport(devicesToReport[i], indicators, primaryWeights);
            if (i < devicesToReport.length - 1) {
                await new Promise(resolve => setTimeout(resolve, 500));
            }
        } catch (err) {
            console.error(`生成设备 ${devicesToReport[i].name} 的报告时出错:`, err);
            alert(`生成设备 ${devicesToReport[i].name} 的报告时出错，请检查控制台。`);
        }
    }
    alert("批量报告生成完成（或已中止）。");
}






/*整个文件做了这样的函数逻辑，如果不理解就跳过：

{
mapScoresToIndicatorNames（指标作为值，分数作为值）函数，这个函数前面没有export，就不能在其他文件中import

逻辑：
传入分数和指标值，对传入的值进行数据判断，bScores是分数，要是对象类型，indicators是指标，要是数组类型
然后使用js内置函数forEach来遍历数组的name元素，对name元素使用match函数+正则的方式来匹配想要的元素，
如果匹配成功，并且bScores存在对应的键值，就映射成功，否则映射失败


可以学到的东西：
forEach内置函数，遍历数组中的每个元素
match+正则： 匹配内容
}




{
async evaluateDevice（评估设备）函数，有export 可以在其他模块中导入

逻辑：
首先传入三个数据（设备的数据，评估指标，评估权重），进行合法性验证，之后调用前面的指标-分数映射函数，对输入设备数据中的分数和评估指标进行映射，映射完存储在scoresForApi中（对象类型）
之后准备数据json格式（即将发送给服务器的数据），包括scoresForApi，originalPrice，secondaryIndicators
使用axios.post向服务器传输数据，服务器会返回一个响应对象，存储在responese中。最后返回设备数据和请求结果。对于请求中出现的错误，在catch中处理

这个响应对象包含下面的东西：
{
  data: {...},         // 服务器返回的具体数据（请求结果）
  status: 200,         // HTTP 状态码，如 200、404、500 等
  statusText: "OK",    // HTTP 状态文本，如 "OK" 或 "Not Found"
  headers: {...},      // 服务器返回的响应头
  config: {...},       // Axios 请求的配置信息
  request: {...}       // 请求对象（有时包含额外信息）
}


可以学到：
对于async（异步）函数，try和catch是必须的，用来处理错误
await是代码执行暂停，直到上一步执行完毕才继续执行后面的语句
axios.post用于向服务器发送请求，还有axios.get()方法

}


{
剩下的三个函数简单理解就是用来生成PDF文档的，具体实现看函数内部，
不展开说明是里面没有vue的语法

}







*/